Skip to content

release: stage audited 7.14.3 Bitcoin-only fork product - #627

Open
BitHighlander wants to merge 90 commits into
developfrom
release/7.14.3-bitcoin-only
Open

BitHighlander wants to merge 90 commits into
developfrom
release/7.14.3-bitcoin-only

Conversation

@BitHighlander

@BitHighlander BitHighlander commented Aug 27, 2026 •

Copy link
Copy Markdown
Owner

Firmware-only scope correction in progress. The bootloader-coupled durability batch is excluded; its prior integration acceptance is withdrawn. Corrected candidates are staged as fork audit PRs #754 (7.14.2), #755 (7.14.3), and #756 (7.15). Fresh firmware/board and pinned-host suites pass all five variants. Exact-head CI/ARM validation and canonical integration remain pending.

The original power-interruption finding remains open with the coupled remediation deferred under owner scope direction; this is not a clean-release claim. Existing signing and independent firmware hardening fixes are retained. No Copilot review, develop merge, bootloader publication or upstream submission is authorized.

Current scope, exact candidates and evidence.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

It changes multiple security-critical subsystems (RNG, seed ceremony, storage/versioning, signing/address semantics, and CI/release provenance) across many files, warranting final human review despite strong test coverage.

Pull request overview

This PR advances the 7.14.x release line to v7.14.3 and implements/locks down the “Bitcoin-only + Dice entropy” release delta while hardening seed-time RNG handling, Taproot/BIP-340/341/86 support, storage compatibility boundaries, and CI/release gating.

Changes:

  • Add Bitcoin-only build variant (compile-time stripping of non-Bitcoin engines, message map entries, coin/token tables, and CI/release matrix coverage for both variants).
  • Add/extend Bitcoin security features: Taproot address/script support, OP_RETURN disclosure hardening, multisig quorum validation, tx input digest reset helper, and new regression/unit tests.
  • Add seed-time RNG health gate plus Dice entropy reset ceremony (with debug-link observability and extensive unit tests), plus CI hardening (gitleaks pin+scoping, aggregate “ci-gate”, nonzero propagation).
File summaries
File Description
unittests/firmware/transaction.cpp New unit tests for taproot input weight, OP_RETURN/Omni disclosure paging, multisig quorum rejection.
unittests/firmware/test_board.cpp One-time board bootstrap guard for unit test binaries to prevent hangs.
unittests/firmware/signing.cpp New unit tests for change-path/script-type constraints, checksum encoding, multisig quorum validation, abort scrubbing.
unittests/firmware/setup_ceremony.cpp Tests for abort wiping mnemonic + recovery fragments; recovery finalize disarm behavior.
unittests/firmware/rng_health.cpp New unit tests for RNG health analysis, chunking equivalence, and fail-closed semantics.
unittests/firmware/fsm.cpp Conditionally exclude non-Bitcoin FSM abort workflow tests from bitcoin-only builds.
unittests/firmware/dice.cpp New tests for dice roll counts and deterministic mix vectors.
unittests/firmware/confirm_test_utils.cpp New confirm auto-accept driver via emulator tiny UDP messages (ButtonAck + DebugLinkDecision).
unittests/firmware/CMakeLists.txt Build manifest refactor + bitcoin-only conditional compilation of altcoin suites.
unittests/firmware/authenticator.cpp New authenticator cache/authorization-boundary tests with confirm driver integration.
unittests/crypto/CMakeLists.txt Add BIP340/BIP341 crypto test compilation.
unittests/crypto/bip340.cpp Add verbatim BIP-340 vectors + BIP-86 address vectors + BIP-341 key-path witness checks.
scripts/verify-token-def.py New generator guard: fail build if generated token defs have zero usable rows.
scripts/emulator/firmware-unit.sh Preserve make xunit exit status and propagate it after copying reports.
scripts/emulator/docker-compose.bitcoin-only.yml New compose overlay to build/run emulator+unit container with KK_BITCOIN_ONLY=ON.
scripts/emulator/capture-dice-flow.py New evidence tool to drive/reset dice flow and capture OLED screenshots.
lib/rand/rng.c Latch STM32 RNG seed/clock errors in software + emulator test seams + strong random_buffer for some linkers.
lib/rand/rng_health.c New RNG source liveness check + SP800-90B RCT/APT + latched verdict + checked draw/continuous observe.
lib/rand/CMakeLists.txt Add rng_health.c to kkrand library sources.
lib/firmware/u2f.c Route U2F key-handle derivation path through checked RNG; fail registration on RNG self-test failure.
lib/firmware/txin_check.c Add txin_dgst_reset_current() helper to preserve last-tx duplicate warning while resetting current digest.
lib/firmware/transaction.c Taproot address encoding, scriptPubKey derivation helper, OP_RETURN confirm behavior for bitcoin-only, multisig quorum validation, taproot input weight.
lib/firmware/ton.c Adjust Ed25519 signing call sites to updated signature (no explicit public-key pointer).
lib/firmware/storage.h Add SUS_BitcoinOnlyLocked status for refusing incompatible storage without wiping.
lib/firmware/storage.c Bitcoin-only storage band stamping/locking, RNG-gated key-material draws, stronger session-clear scrubbing, refuse commit when locked.
lib/firmware/solana.c Adjust Ed25519 signing call sites to updated signature (no explicit public-key pointer).
lib/firmware/reset.c Add dice-entropy reset flow, roll digest lifecycle, RNG health failure reporting, and broader abort scrubbing.
lib/firmware/recovery_cipher.c Hoist/rename scratch buffers to make abort wiping reliable; add debug test seams for fragment wiping.
lib/firmware/messagemap.def Exclude non-Bitcoin message handlers and outputs in bitcoin-only builds.
lib/firmware/fsm.c Centralize derived-node scrub, bitcoin-only no-op abort hooks, and bitcoin-only “locked wallet” refusal in init flows.
lib/firmware/fsm_msg_solana.h Adjust Ed25519 signing call site to updated signature.
lib/firmware/fsm_msg_debug.h Expose dice digest via DebugLinkState.
lib/firmware/fsm_msg_common.h Advertise taproot capability; set stable bitcoin-only variant strings; bound GetCoinTable to build variant; tighten wipe/session boundary.
lib/firmware/fsm_msg_coin.h Enforce BIP86 (m/86’) path/script-type invariants for Taproot address requests.
lib/firmware/dice_input.c New on-device dice entry UI/flow with debounce/hold classification and debug-link injection support.
lib/firmware/coins.c Exclude tokens from coin table in bitcoin-only builds; add BIP86 path mismatch checks.
lib/firmware/CMakeLists.txt Conditional compilation/linking of non-Bitcoin engines; add dice input; add token-table verification step.
lib/firmware/authenticator.c Add cache wipe/test seams; harden OTP generation cleanup/zeroization paths.
lib/firmware/app_layout.c Fix multi-line bech32 address rendering so displayed address doesn’t silently truncate.
lib/board/keepkey_flash.c Gate OTP randomness-block programming on checked RNG draw; fail closed without locking bad entropy.
include/keepkey/transport/messages.options Add dice_digest sizing; bound DebugLinkDecision.input to keep tiny-message decode within 64B; size clearsign fields.
include/keepkey/transport/messages-thorchain.options Add ThorchainMsgSend.denom sizing.
include/keepkey/transport/messages-solana.options Add Solana sizing entries for schema/LUT/signature fields and other buffers/counts.
include/keepkey/transport/messages-ripple.options Add RippleSignTx.memo size limit.
include/keepkey/transport/messages-ethereum.options Add LoadClearsignSigner size limits.
include/keepkey/rand/rng.h Add seed/clock error latch API + emulator test seams; include stdbool.
include/keepkey/rand/rng_health.h New public RNG health/checked-draw API and explicitly scoped coverage list.
include/keepkey/firmware/txin_check.h Export txin_dgst_reset_current().
include/keepkey/firmware/transaction.h Export multisig quorum predicate + input scriptPubKey helper.
include/keepkey/firmware/storage.h Define shipped version floor + bitcoin-only version band + locked-wallet query API.
include/keepkey/firmware/signing.h Expose pure helpers for unit tests + debug seams for state wipe checks.
include/keepkey/firmware/reset.h Extend reset_init signature to include dice_entropy; add dice digest accessor.
include/keepkey/firmware/recovery_cipher.h Add debug test seams for recovery fragment wiping verification.
include/keepkey/firmware/fsm.h Export derived-node scrub + debug test seams.
include/keepkey/firmware/dice_input.h New dice-entropy API surface (roll collection + mix).
include/keepkey/firmware/coins.h Exclude token indices from bitcoin-only builds.
include/keepkey/firmware/coins.def Exclude non-Bitcoin coin definitions from bitcoin-only builds.
include/keepkey/firmware/authenticator.h Add cache/test seam declarations and include stdbool.
docs/security/7.14.3-bitcoin-only-dice-audit-sop.md New audit SOP defining line-review ledger, gates, and evidence expectations.
docs/DiceEntropy.md New user-facing doc describing dice entropy flow, guarantees, and limitations.
deps/crypto/CMakeLists.txt Add bip340.c to trezor-crypto build.
CMakeLists.txt Bump version to 7.14.3; add KK_BITCOIN_ONLY option; ensure BITCOIN_ONLY define is always 0/1; disable unused coin crypto in bitcoin-only build.
.github/workflows/release.yml Release matrix for full + bitcoin-only artifacts; per-variant hash manifests with provenance.
.github/workflows/ci.yml Least-privilege permissions; pinned/scoped gitleaks; build/test matrix for both variants; compose overlays; per-variant artifacts; global ci-gate job.
Review details
  • Files reviewed: 68/68 changed files
  • Comments generated: 0
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@BitHighlander
BitHighlander force-pushed the release/7.14.3-bitcoin-only branch from 6024f60 to 0f7903d Compare August 27, 2026 20:02
@BitHighlander

BitHighlander commented Aug 28, 2026 •

Copy link
Copy Markdown
Owner Author

Audit index — 7.14.3 Bitcoin-only

This is the durable top-level ledger. Detailed checkboxes live in the commit-pinned line maps below. A checkbox means every listed base and candidate range in that file was reviewed; findings must cite the exact range and head.

Immutable review identity

Item Exact identity
Base review/7.14.2-pr458-current @ 3df4038f48a5c8d2656d71749e7f40a989f979c2
Candidate release/7.14.3-bitcoin-only @ abe29d1288638867dc64dfe04219b89f7a593133
History shape one squashed release commit on the immutable base
Pre-squash preservation preserve/release-7.14.3-pre-upstream-squash-20260828 @ c636edfa
Firmware diff 117 files, +6,600 / −733

Canonical dependency identity

Dependency Previous release pin Candidate pin Internal line map
keepkey/trezor-firmware 03d8a55a cdc05beb 57 files / checkboxes
keepkey/device-protocol f2c3c005 8545cd5b 21 files / checkboxes
keepkey/python-keepkey 92e8745e 9c398203 81 files / checkboxes
Python's canonical DP companion baseline PR #112 @ 27d3fa1f audited in its upstream PR; not compiled directly by this firmware

The direct firmware DP pin intentionally remains 8545cd5b: compiling PR #112 directly in this release produces unsupported nanopb callback fields. Every committed submodule URL is canonical upstream.

Line-addressable audit surfaces

Gates

  • All 276 line-map checkboxes have an identified reviewer and are complete.
  • Findings are linked to fixes or explicit technical dispositions.
  • Exact-head CI run 33201418360 is fully green for abe29d12.
  • Fresh exact-head Copilot review converges after the squash.
  • RC1 physical Dice/setup, signing, product-boundary and upgrade/downgrade testing is complete.
  • Canonical DP/Python human reviews and merges are complete; firmware is repinned if their merge object changes.

Head-change log

Old → new Surfaces Disposition
c636edfa → abe29d12 all firmware audit identities History-only squash; git diff confirms the firmware trees are byte-identical. Old head preserved. All line links regenerated for the new commit; exact-head CI and Copilot review invalidated/restarted.

Copilot delivery status

Date Request result Exact-head result Gate
2026-08-28 Post-squash review request API accepted No Copilot review_requested timeline event and no review on abe29d12; the only Copilot review remains stale at 64e9ce1c OPEN — verify repository Copilot code-review policy / AI-credit availability, then request a fresh review on the exact head

No merge, tag, signing, publishing, release, upstream firmware PR, or physical-test claim is made by this fork audit vehicle.

Squash the audited 7.14.3 delta onto the immutable 7.14.2 review base: Bitcoin-only product boundaries, Taproot signing, Dice entropy and RNG health, storage compatibility, disclosure and signing fixes, dependency pins, tests, and release evidence gates.
@BitHighlander
BitHighlander force-pushed the release/7.14.3-bitcoin-only branch from c636edf to abe29d1 Compare August 28, 2026 18:54
@BitHighlander

Copy link
Copy Markdown
Owner Author

7.14.3 line audit — firmware delta

Exact range: 3df4038f48a5c8d2656d71749e7f40a989f979c2 → abe29d1288638867dc64dfe04219b89f7a593133. Every link is commit-pinned.

Checkbox rule: mark a file complete only after reviewing every listed base and candidate range, including removed/replaced base lines. Findings must cite the surface, path, range, and exact head. Generated files and tests are reviewable evidence, not substitutes for reviewing runtime code.

Changed files represented in this checklist: 117.

7143-BTC — Bitcoin, Taproot, signing and exact disclosure

7143-CI — CI, release provenance, artifacts and failure propagation

7143-DEP — dependency identity and build integration

7143-DOC — security contract and operator documentation

7143-ENT — Dice, RNG, setup, storage and secret lifecycle

7143-PROD — Bitcoin-only product boundary and link surface

7143-REG — cross-chain and regression validation

7143-REG — cross-chain, parser, UI and session hardening

@BitHighlander

Copy link
Copy Markdown
Owner Author

7.14.3 line audit — trezor-crypto dependency

Exact range: 03d8a55a832fb61bb89477ef7239a80ecb367080 → cdc05bebe9e6989cf711e1b5bea6324fd09f848e. Every link is commit-pinned.

Checkbox rule: mark a file complete only after reviewing every listed base and candidate range, including removed/replaced base lines. Findings must cite the surface, path, range, and exact head. Generated files and tests are reviewable evidence, not substitutes for reviewing runtime code.

Changed files represented in this checklist: 57.

DEP-CRYPTO-BTC — secp256k1, Schnorr and Taproot primitives

DEP-CRYPTO-CORE — shared hashes, encodings and build surface

DEP-CRYPTO-ED — Ed25519 API and callers

DEP-CRYPTO-TEST — dependency tests and vectors

DEP-CRYPTO-ZEC — Pallas, RedPallas and Orchard primitives

@BitHighlander

Copy link
Copy Markdown
Owner Author

7.14.3 line audit — device-protocol dependency

Exact range: f2c3c005ad824df11aec9592a8b62066323ba282 → 8545cd5b615f5832374afbf06387a3f28869285e. Every link is commit-pinned.

Checkbox rule: mark a file complete only after reviewing every listed base and candidate range, including removed/replaced base lines. Findings must cite the surface, path, range, and exact head. Generated files and tests are reviewable evidence, not substitutes for reviewing runtime code.

Changed files represented in this checklist: 21.

DEP-DP-BOUNDS — nanopb capacity and allocation bounds

DEP-DP-GEN — generators, documentation and repository wiring

DEP-DP-SCHEMA — protobuf message and wire-ID definitions

@BitHighlander

Copy link
Copy Markdown
Owner Author

7.14.3 line audit — Python companion dependency

Exact range: 92e8745e56985a05049a0f458226a29f94ca3532 → 9c3982035664dbec44c6d1d60db6edb9fa59713c. Every link is commit-pinned.

Checkbox rule: mark a file complete only after reviewing every listed base and candidate range, including removed/replaced base lines. Findings must cite the surface, path, range, and exact head. Generated files and tests are reviewable evidence, not substitutes for reviewing runtime code.

Changed files represented in this checklist: 81.

DEP-PYK-CI — companion CI, catalogs and generation gates

  • .github/workflows/ci.yml — base: base: L3-L10, L16-L18, L50-L53, L59-L67, L83, L102-L117, L123-L124, L129, L176-L181, L186; candidate: candidate: L3-L12, L18-L31, L42-L43, L52-L76, L87-L96, L102-L114, L120-L162, L169, L174, L179, L185, L190-L263, L269-L270, L275, L322-L762, L767
  • .github/workflows/copilot-review.yml — base: base: L4; candidate: candidate: L4-L7, L12
  • scripts/generate-test-report.py — base: base: L75, L177-L180, L190-L228, L243, L259, L271-L293, L301, L322, L330, L336, L341-L347, L354-L358, L367-L369, L377-L379, L394, L423-L434, L461, L546-L547, L669-L693, L698-L699, L707-L708, L715-L717, L752-L753, L776-L777, L822-L826, L844-L846, L891-L899, L904-L906, L923-L927, L932-L934, L942, L958-L960, L974-L976, L981, L988, L1004, L1012, L1029-L1032, L1040, L1047-L1056, L1083-L1197, L1218, L1225, L1231, L1237, L1243, L1255-L1290, L1295-L1324, L1361-L1370, L1377-L1392, L1405-L1407, L1416-L1420, L1428-L1432, L1463-L1472, L1477, L1483-L1485, L1513-L1517, L1548, L1559-L1567; candidate: candidate: L15-L29, L90, L156-L169, L206-L234, L243-L346, L359-L374, L385-L390, L396-L406, L422-L520, L541, L549, L555, L560-L576, L583-L587, L596-L598, L606-L608, L623, L630-L644, L667, L697, L782-L787, L810-L898, L998-L1057, L1062-L1067, L1075-L1076, L1083-L1085, L1120-L1122, L1131-L1209, L1224-L1225, L1253-L1312, L1330-L1341, L1359-L1381, L1426-L1447, L1452-L1454, L1469-L1886, L1891-L1899, L1907, L1923-L1927, L1941-L2065, L2070, L2077, L2093, L2101, L2118-L2160, L2166, L2173-L2320, L2347, L2368, L2375, L2381, L2387, L2393, L2400-L3069, L3074-L3113, L3145, L3151-L3225, L3244-L3248, L3257-L3261, L3269-L3273, L3278-L3305, L3332-L3341, L3346, L3352-L3355, L3362-L3368, L3390-L3402, L3433, L3443-L3455

DEP-PYK-DOC — packaging, fixtures and documentation

DEP-PYK-LIB — host API, protobuf bindings and transaction logic

  • keepkeylib/clearsign_abi.py — base: —; candidate: candidate: L1-L139
  • keepkeylib/clearsign_catalog.py — base: —; candidate: candidate: L1-L1009
  • keepkeylib/client.py — base: base: L993-L996, L1001, L1136-L1145, L1317, L1691-L1694, L1789-L1790, L1809-L1815, L1833, L1868, L1878-L1891; candidate: candidate: L52, L63, L465-L484, L748-L780, L809-L867, L1107-L1140, L1145-L1146, L1281-L1310, L1482-L1486, L1639-L1641, L1684-L1686, L1866-L1880, L1975-L2010, L2026-L2039, L2052-L2069, L2075-L2086, L2107-L2110, L2117-L2184, L2190-L2204, L2212-L2309
  • keepkeylib/debuglink.py — base: —; candidate: candidate: L90-L93, L134-L140
  • keepkeylib/eip712_stream.py — base: —; candidate: candidate: L1-L308
  • keepkeylib/eth/ethereum_tokens.py — base: base: L47; candidate: candidate: L47-L67
  • keepkeylib/eth/token_policy.py — base: —; candidate: candidate: L1-L137
  • keepkeylib/eth/uniswap_tokens.py — base: base: L31; candidate: candidate: L30-L52
  • keepkeylib/hive.py — base: —; candidate: candidate: L1-L87
  • keepkeylib/mapping.py — base: base: L100; candidate: candidate: L16, L26-L29, L105-L129
  • keepkeylib/messages_ethereum_pb2.py — base: base: L23, L469-L470, L514-L515, L552-L553, L597-L598, L656-L657, L715-L716; candidate: candidate: L23, L29-L74, L482-L554, L588-L589, L633-L634, L671-L672, L716-L717, L775-L776, L834-L1102, L1110, L1117-L1121, L1173-L1179, L1222-L1272
  • keepkeylib/messages_hive_pb2.py — base: —; candidate: candidate: L1-L886
  • keepkeylib/messages_pb2.py — base: base: L24, L347-L779, L785-L786, L962, L1217, L1254-L1255, L1299-L1300, L1323-L1324, L1382-L1383, L1413-L1414, L1472-L1473, L1503-L1504, L1541-L1542, L1579-L1580, L1603-L1604, L1634-L1635, L1665-L1666, L1689-L1690, L1713-L1714, L1744-L1745, L1775-L1776, L1806-L1807, L1865-L1866, L1903-L1904, L1962-L1963, L1993-L1994, L2017-L2018, L2097-L2098, L2184-L2185, L2208-L2209, L2239-L2240, L2333-L2334, L2357-L2358, L2388-L2389, L2426-L2427, L2471-L2472, L2523-L2524, L2575-L2576, L2613-L2614, L2672-L2673, L2717-L2718, L2769-L2770, L2807-L2808, L2880-L2881, L2911-L2912, L2949-L2950, L2980-L2981, L3067-L3068, L3112-L3113, L3143-L3144, L3174-L3175, L3226-L3227, L3271-L3272, L3302-L3303, L3347-L3348, L3392-L3393, L3423-L3424, L3461-L3462, L3492-L3493, L3516-L3517, L3540-L3541, L3578-L3579, L3609-L3610, L3633-L3634, L3755-L3756, L3779-L3780, L3824-L3825, L3848-L3849, L3879-L3880, L4787-L4788; candidate: candidate: L24, L347-L907, L913-L914, L997-L1002, L1096-L1100, L1116-L1137, L1364-L1370, L1384, L1421-L1422, L1466-L1467, L1490-L1491, L1549-L1550, L1580-L1581, L1639-L1640, L1670-L1671, L1708-L1709, L1746-L1747, L1770-L1771, L1801-L1802, L1832-L1833, L1856-L1857, L1880-L1881, L1911-L1912, L1942-L1943, L1973-L1974, L2032-L2033, L2070-L2071, L2129-L2130, L2160-L2161, L2184-L2185, L2264-L2265, L2339-L2345, L2358-L2359, L2382-L2383, L2413-L2414, L2507-L2508, L2531-L2532, L2562-L2563, L2600-L2601, L2645-L2646, L2697-L2698, L2749-L2750, L2787-L2788, L2846-L2847, L2891-L2892, L2943-L2944, L2981-L2982, L3054-L3055, L3085-L3086, L3123-L3124, L3154-L3155, L3241-L3242, L3286-L3287, L3317-L3318, L3348-L3349, L3400-L3401, L3445-L3446, L3476-L3477, L3521-L3522, L3566-L3567, L3597-L3598, L3635-L3636, L3666-L3667, L3690-L3691, L3714-L3715, L3752-L3753, L3771-L3777, L3790-L3791, L3814-L3815, L3924-L3930, L3943-L3944, L3967-L3968, L4012-L4013, L4036-L4037, L4067-L4192, L4282-L4285, L4758-L4785, L4945-L4956, L5143-L5152, L5183-L5226
  • keepkeylib/messages_ripple_pb2.py — base: base: L22, L159, L203-L204, L241-L242; candidate: candidate: L22, L146-L152, L166, L210-L211, L248-L249
  • keepkeylib/messages_solana_pb2.py — base: base: L22, L145, L196-L197, L227-L228, L279-L280, L317-L318, L383-L384, L421-L422; candidate: candidate: L22, L132-L145, L159, L198-L246, L259-L260, L290-L291, L342-L343, L380-L381, L446-L447, L484-L485
  • keepkeylib/messages_thorchain_pb2.py — base: base: L23, L303, L354-L355, L392-L393; candidate: candidate: L23, L290-L296, L310, L361-L362, L399-L400
  • keepkeylib/messages_zcash_pb2.py — base: base: L22, L134, L153, L274-L275, L305-L306, L343-L344, L388-L389, L433-L434, L454, L485-L492, L498-L500, L505, L523-L524, L549-L571, L588-L589, L619-L620, L630, L684-L689; candidate: candidate: L6, L23-L50, L160-L209, L215-L221, L235, L344-L357, L370-L371, L401-L402, L439-L440, L484-L485, L517-L523, L536-L582, L602, L621-L648, L661-L668, L674-L676, L681, L699-L734, L757-L758, L775-L776, L794-L800, L813-L817, L824-L830, L875-L881, L889-L901
  • keepkeylib/signed_metadata.py — base: base: L131, L140, L145, L154-L158, L189, L198, L232-L265, L283-L286; candidate: candidate: L23-L61, L170, L179-L182, L187-L193, L202-L209, L240-L241, L250-L252, L268-L381, L399-L433, L451-L457, L492-L666
  • keepkeylib/transport_udp.py — base: base: L60; candidate: candidate: L5, L10-L33, L59-L60, L81-L86, L93-L105
  • keepkeylib/types_pb2.py — base: base: L24, L397, L423-L424; candidate: candidate: L24, L393-L396, L401, L427-L428, L504
  • keepkeylib/zcash.py — base: —; candidate: candidate: L1-L44

DEP-PYK-TEST — device integration, vectors and report coverage

@BitHighlander
BitHighlander changed the base branch from review/7.14.2-pr458-current to develop September 8, 2026 19:59
BitHighlander and others added 29 commits September 9, 2026 21:51
Picks up the reset-ceremony fix: the Internal Entropy screen is branched on the
actual per-product policy instead of a pre-7.15 version floor. On 7.14.2 the
old predicate expected a ButtonRequest that the product deliberately does not
send, which failed mid-ceremony and left the emulator in PIN entry, cascading
into the next test's wipe_device.

Canonical keepkey:reconcile/upstream-sync @ 45e19bc582cd4eba2c0703eec76ac52e5564f7e1.
Floors the taproot must-run entries at 7.14.3, the release taproot ships in,
instead of 7.0.0. Applied to 7.14.2 the old floor demanded six taproot cases
from a product with no supports_taproot and no P2TR path, which validate-junit
failed as skipped-but-required. The requirement stays binding on 7.14.3 and
7.15, which both carry taproot.

Canonical keepkey:reconcile/upstream-sync @ 8649abeaeb8f60543e162bb3ebcd8bf25243d70c.
7.14.3 was the only product still honouring ResetDevice.display_random. 7.14.2
ignores it outright and 7.15 removed the parameter, so the newest of the three
re-opened a disclosure both its neighbours close. Fork issue #542.

The screen rendered all 32 bytes of the device RNG draw and was drawn strictly
BEFORE EntropyRequest, so it disclosed the exact half whose complement the host
itself supplies: anyone reading the OLED who also knows ext_entropy computes
SHA256(shown || ext_entropy), the mnemonic pre-image. Revealing before the
counterparty commits is a commitment scheme run backwards.

Two in-tree claims were wrong and are corrected rather than copied:
  - "the displayed value is already post-mix" — false on every reachable path,
    since display_random and dice_entropy were mutually exclusive, so dice_mix()
    never ran when the screen was drawn. The value shown was the raw RNG draw.
  - the implied Trezor lineage. Trezor's legacy firmware had byte-for-byte this
    same ordering and removed the feature in PR #4119 (core 2.8.3, legacy
    1.13.0), calling it purposeless and a false sense of security.

Nothing shipped requests it: hdwallet answers EntropyRequest from
crypto.getRandomValues inside the transport and never surfaces the value, so the
audit the screen was supposed to enable could not be completed on our own host
stack even in principle. The field stays in the wire schema and is ignored, as
on 7.15 — it is not reserved, because keepkeyctl still passes it positionally.

Removes both guards that existed only to constrain the screen (the _no_backup
refusal and the dice mutual exclusion), which also repairs
scripts/emulator/capture-dice-flow.py: it requested display_random and
dice_entropy together, so the guard answered it with a SyntaxError and the
script could not run against its own firmware.

reset.c, reset.h and the dice block are now identical to 7.15 apart from 7.15's
BIP-85 mnemonic scratch. dice_input.c was already byte-identical.

docs/DiceEntropy.md gains a ColdCard comparison and states the remaining gap
plainly: a user can prove their rolls were captured, but not that they reached
the seed, because seed = SHA256(SHA256(int_entropy || rolls) || ext_entropy)
holds two values the user does not have.
display_random is now ignored on every product, so the reset tests assert the
device does not draw an internal-entropy screen instead of branching on version.

Canonical keepkey:reconcile/upstream-sync @ 7f538a95f00fdea59879ceab1a684762fe408411.
Side-by-side of the two dice models, written to answer one question: can a user
prove the device used their rolls? Ours says no, and the reason is the
derivation, not the dice-entry UI, which is comparable.

  KeepKey:  seed = SHA256(SHA256(int_entropy || rolls) || ext_entropy)
  ColdCard: seed = SHA256(rolls)                        (Dice-Rolls-Only)

Three inputs against one. The user holds exactly one of ours, and ext_entropy
is generated inside the hdwallet transport and never surfaced, so it is not
recoverable even by a cooperating user.

Records why ColdCard can safely display its own pre-mix TRNG entropy while our
equivalent screen could not: their protocol has no entropy command, so the
complement of anything shown is dice the user never transmits. The rule is
about the complement, not about showing.

Also records the July 2026 ColdCard RNG disclosure, where a five-year PRNG
regression cut the effective search space to about 72 bits and the one
mitigating factor was users who had rolled dice. That is the argument for a
dice-only mode: mixing protects you when your dice are bad, dice-only protects
you when the device is bad, and the second is the failure that actually
happened to a shipping vendor.

Proposes dice_only = 11 on ResetDevice, an offline verifier, the full 32-byte
digest, roll bias rejection, and that dice-only never be the default.

Fixes a stray double blank line left in reset.c by the display_random removal.
…everywhere

Builds no longer wait on static analysis: `needs` is the three fast gates
(lint-format, check-submodules, secret-scan) and cppcheck runs beside them,
multi-threaded, with its build dir cached per commit (prefix restore -- the
build dir re-checks each file's own hash, so a stale entry is safe).

The base image is pulled once, with retries, and loaded from the cache in
every job that needs it; the integration job builds the compose services
once and runs the two containers without --build. The exit codes are
initialised before `|| RC=$?` (which assigns only on failure), and the
clang-format install retries because apt.llvm.org intermittently serves an
empty key and lint-format now gates the whole graph.

Measured on the fork: 10.1 min -> 7.4 min wall for 7.14.3.
Every one is a regression this release introduced, or a claim it makes that
the code does not keep. Each has a test that fails without the fix -- run
against a control build with the fixes reverted, not just against the fix.

Auto-lock, which this release extended to AWAY_FROM_HOME:
- idle_time is never reset by host traffic, so it measured wall-clock since
  the device left the home screen rather than idleness. A cipher recovery or
  a large signing stream that outlasted the delay was torn down mid-flight
  ("Not in Recovery mode" after 24 words of entry). Host frames now count as
  activity, but only AWAY_FROM_HOME, so polling a device sitting at the home
  screen still cannot hold it unlocked.
- the lock path aborts signing, and signing_abort() draws the home screen,
  which reset the timer it had just fired on. The screensaver was replaced by
  the home screen one tick later, on a device that had just locked. The timer
  is preserved across the abort.

PIN handling:
- pin_protect() checks every entered PIN against the wipe code, and any PIN
  that is not the wipe code returns PIN_WRONG, which cleared the session --
  which this release made abort all workflows. A dry-run recovery stages its
  ceremony before prompting, so on a PIN-protected device it disarmed itself
  and the first CharacterAck failed. PIN revocation now revokes signing
  state; the paths that must end a ceremony (session_clear, the auto-lock,
  Initialize, ClearSession) still call fsm_abort_workflows() themselves.

Amounts and rendering:
- the XRP ceiling was 1e11 drops (100,000 XRP), 1e6 below XRP's own maximum,
  so payments 7.14.1 signed correctly were refused with a SyntaxError. The
  encoding has 62 usable bits; 1e17 drops fits with room to spare.
- MsgDeposit carries "MAYA.CACAO", not "cacao", so every CACAO deposit
  rendered at zero decimals: 1 CACAO showed as "10000000000 MAYA.CACAO"
  while the same base units showed as "1 cacao" on the MsgSend screen.
- the seed-backup pager measured mnemonic rows into a stack buffer it never
  scrubbed, leaving up to 351 bytes of backup words behind.

Claims the code did not keep:
- storage_commit() CRCs sizeof(flash_temp)/4 WORDS, and 2570 bytes is 642
  words = 2568 bytes, leaving the last byte of the V17 record outside the
  write verification -- while a unit test and a comment asserted the opposite.
  The buffer is 2572 bytes (643 words) with static assertions, as 7.15 has.
- layout_debuglink_watermark() was deleted while tools/bootloader still calls
  it under DEBUG_LINK, so no debug device image could be built. The
  definition is restored, still uncalled by layout_clear().

Verified in the CI container: firmware-unit 204/204, board-unit 19/19,
crypto-unit 18/18.
…sg.value

Two defects in the THORChain EVM deposit path, both already fixed on the 7.15
line and missing here.

thor_isThorchainTx() matched on the 4-byte selector alone, so calldata shaped
like depositWithExpiry() to ANY contract on ANY chain got the THORChain
clear-sign screens -- and with them the exemption from the AdvancedMode gate
that blocks every other unrecognized contract call. A default device would
narrate "Routing through <attacker hex>", "Using Asgard vault <attacker hex>",
"Confirm sending 1 ETH" and sign. The pin is (chain_id, address) together,
because a router address is an identity on one chain only: Ethereum
d37bbe57.. (the current v4.1.1 router; the constant still named the retired v3
address, so the "Thorchain router" label was already dead) and Avalanche
00dc6100.., the two the test suite exercises. A chain with no pinned router
falls to the blind-sign gate rather than inheriting the trusted UX.

For a native-asset deposit the router forwards msg.value and ignores the ABI
amount word, but the amount screen rendered the ABI word. A host could show
"Confirm sending 0.01 ETH" on the decoder's own screen while attaching 5 ETH,
which only appeared later on the generic fee screen. Native deposits now
display msg.value, and a token deposit with value attached is refused -- that
value would be swept with no screen representing it.

Ethereum.ThorchainDepositIsPinnedToItsRouterOnItsChain covers both routers,
the wrong-address, wrong-chain, unpinned-chain and no-chain_id cases. Verified
against a control build with the pin removed, where it fails. Full suite in
the CI container: 205/205.
recovery_cipher_finalize() gated the commit on words_entered, which counts
SEPARATORS, but built the phrase with strtok(), which collapses runs of them.
A ceremony driven with nothing but spaces therefore satisfied the 12/18/24
count gate while emitting zero words: the auto-complete loop never ran (so its
gate stayed true), enforce_wordlist is false when the host omits it (the wire
default), which skips mnemonic_check() in the commit condition, and
setup_commit() stored the EMPTY mnemonic as the device seed and answered
"Device recovered". That seed is PBKDF2("", "mnemonic") -- public. Any funds
sent to the "recovered" wallet are spendable by whoever sent the spaces.

The same shape accepts a one-word phrase. Count the words the loop actually
emitted and require that to equal the count the ceremony claimed, on every
path including the dry run.

Recovery.SpacesOnlyCeremonyIsRefusedAndCommitsNothing drives the ceremony
end to end -- init, twelve space CharacterAcks, finalize -- and asserts the
device is still uninitialized. Against a control build without this fix it
fails with storage_isInitialized() == true, i.e. the seed was committed.
Full suite in the CI container: 206/206.
The deposit and send screens each decided how to scale an amount. 7.15's send
screen drifted from the formatter's rule and scaled every host-chosen denom by
CACAO's 1e10, showing an amount the signed document did not contain; the same
two-copies shape exists here. Extract mayachain_decimalsForDenom() so every
MAYAChain screen asks the same function.

No behaviour change on this line: the formatter already applied 10 decimals to
CACAO and 0 to everything else, and the test pins that.
Pinning the deposit decoder to THORChain's routers refused Maya Protocol
deposits, which use the same calldata shape through their own mainnet router
(e3985e6b..) -- the pinned integration suite calls that address "firmware-
pinned" and its two MAYAChain EVM tests went red. Both protocols are narrated
by these screens, so both routers belong in the pin, and the screen now names
which one it matched instead of hard-coding "Thorchain router".

The chain scope is unchanged: the Maya router is an Ethereum-mainnet identity,
so the same 20 bytes on another chain do not match.
- a bitcoin-only-locked device refused settings writes only inside
  storage_commit(), which returns void: ChangePin, ChangeWipeCode,
  ApplySettings and ApplyPolicies took the user through their confirmations
  and reported success while nothing was written. Guard the four handlers up
  front, the way 7.15 already does, and leave the commit-side check as the
  backstop it was meant to be. (ChangeWipeCode's body is behind an ifdef that
  is off today; the guard is inert until it is turned on.)
- the transport failure handler aborted every workflow in production, which
  discards a setup ceremony a transport hiccup has nothing to do with. It ends
  signing, as it was meant to.
- "Maximum priority fee" could be lower than what the runtime charges: the
  implicit compute-unit limit assumed 200,000 units for every instruction and
  ignored the 3,000-unit builtin allocation (SIMD-0170), so the screen was not
  an upper bound. 7.15 has the same defect and gets the same fix separately.
- the boot RNG gate was never entered by its own tests: they exercised a seam
  that latches regardless, so removing the gate would not have failed them.

Verified in the CI container, BOTH variants: firmware-unit 210/210 full,
107/107 bitcoin-only, board-unit 19/19.
…others

7.14.2 and 7.15 refuse to link when the gap between the end of static
allocation and the stack falls below 16 KiB -- the gate that exists because
RC7 shipped an 11.2 KB gap under a 12.4 KB msg_write() frame, which overwrites
static memory on the boot path. This line carried the .confidential size
assert but not the runtime one, so the same regression could ship here and
only surface as a boot fault on hardware.
… pointer

The ARM build refuses `char* conf = <string literal>` under
-Werror=discarded-qualifiers; the emulator's clang build does not, so the
router-label change compiled clean locally and broke build-arm-firmware. Same
class as the note in the tree: an emulator build proves nothing about ARM.
The recorded pins and validated head predated the audit, and the receipt named
a device-protocol commit that cannot build the shipped dice ceremony. Replace
them with the head, tree, pins and CI run that were actually verified, state
the host results from that run, and record what the audit covered and fixed on
this line.

Documentation only: the validated head named in the receipt is this commit's
parent, and this commit changes no code, test or pin.
The dice audit SOP stated invariants the shipped two-mode ceremony
deliberately does not hold: the modes are opt-in and host-selected, and DICE
ONLY drops device entropy by design so the seed can be recomputed offline from
the rolls alone. A document that contradicts the firmware is worse than none,
because the next auditor reads it as the specification and files the
difference as a defect -- or, worse, "corrects" the firmware to match it.

The bitcoin-only release asset also shipped a reproduction command that builds
the other product's binary, so a reader following it gets a hash mismatch and
concludes the release is unreproducible.
Names the head, tree and CI run this candidate was validated at, itemises the
skips rather than summing them, and records every audit finding on this line
with its resolution in audit-units/. Nothing but documentation changes here,
so the validated head named inside is this commit's parent.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V1RPHNqU3JmYFwBYvEKxKn
`node` is the shared fsm_derived_node scratch, and it is scrubbed only by the
next derivation or by fsm_abort_workflows(). Neither runs on the two error
exits after the derivation: a compile error is answered with
ethereum_signing_abort(), which scrubs ethereum.c's own privkey and nothing
else. So a host that sends an ERC-20 transfer whose encoded recipient does not
match the derived to_address_n leaves a private key sitting in a static buffer,
on demand.

7.14.2 already fixed this; the hunk did not travel. This is that hunk, comment
included, so the three lines read the same.

No seam: fsm_derived_node is static in fsm.c and the transfer path has no
native driver. The commit message says so rather than shipping a test that
would assert nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V1RPHNqU3JmYFwBYvEKxKn
sig_with_hashtype[73] holds a signature of up to 73 bytes -- every size nanopb
can decode for MultisigRedeemScriptType.signatures -- and then appends the
sighash byte at [sig_len], which at 73 is one past the end. The comment above
it says this "removes the out-of-bounds write itself rather than relying on
that cap", and it did not: signing_validate_input()'s 72-byte cap was still the
only thing in the way. Sizing the scratch from the field makes the claim true
and cannot drift if the cap ever moves.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V1RPHNqU3JmYFwBYvEKxKn
ethereumFormatAmount() takes the Wanchain tx type from a module static that
ethereum_signing_init() owns. On the transfer path the amount screen is drawn
BEFORE signing_init() runs, so a Wanchain transaction left its type behind and
the next transfer's amount screen named the asset " WAN" on whatever chain it
was really on -- a wrong ticker beside a right number, which is the shape of
mistake a confirmation screen exists to prevent.

The formatter now sets that type from the message it was handed.
signing_init() assigns the same value again later, so nothing downstream
changes.

Ethereum.TransferTickerComesFromThisMessageNotTheLastOne pins it, with the
Wanchain leg as an in-test control so a build that never set the ticker could
not pass the Ethereum assertion for the wrong reason. The test asserts the
ticker is derived from the message passed in; reproducing the stale-static
sequence itself needs the fsm path, which has no native driver.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V1RPHNqU3JmYFwBYvEKxKn
…before

rng_health_check() runs before random_buffer() and proves only that the source
was sound when the draw started. A seed or clock fault that latches while
these very bytes are being produced was noticed on the NEXT call, after this
one had already been handed to the caller -- and this path exists precisely to
feed key material.

rng_health_gate() re-reads SEIS/CEIS on every sampling iteration for exactly
this reason. This applies the same rule on the consumer side, and wipes the
buffer the way the continuous-test refusal beside it already does.

No seam: the test hooks latch the mirror between calls, not during one, so
there is no way to trip this from the native harness without adding a hook to
production code for the test's benefit. The existing latch tests still pin the
pre-draw check.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V1RPHNqU3JmYFwBYvEKxKn
tiny-json.c defined `int errno = 0;` and its header declared it, which is a
reserved identifier: on glibc and MinGW `errno` is a macro expanding to a
function call, so the definition does not compile there, and anywhere it does,
a translation unit that includes <errno.h> gets two different things under one
name. It is write-only state -- nothing ever reads it.

7.14.2 and 7.15 both renamed it to json_errno; this line took only half the
header change (the extern "C" wrapper) and left the name. This is 7.14.2's
version, so the sibling bitcoin-only lines match.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V1RPHNqU3JmYFwBYvEKxKn
`git submodule update --remote` is in this repo's documented workflow, and it
moves a submodule to the tip of the branch named in .gitmodules. Several keys
named a branch that does not contain the commit actually pinned, so running
the documented command silently REWINDS a release pin -- for python-keepkey on
7.14.2 and 7.15, and for device-protocol on 7.14.3 and 7.15, where `master`
does not carry the release protocol at all.

Each key now names a branch verified to contain this tree's pin. Where that
branch is a PR branch rather than canonical, the receipt says so and says when
to re-pin.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V1RPHNqU3JmYFwBYvEKxKn
Names the head, tree and CI run this candidate was validated at, itemises the
skips rather than summing them, states that every .gitmodules branch key was
verified to contain the pin above, and records every audit finding on this
line with its resolution in audit-units/ -- including the second pass over the
P3s the first pass had triaged rather than re-checked.

Nothing but documentation changes here, so the validated head named inside is
this commit's parent.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V1RPHNqU3JmYFwBYvEKxKn
… consent

Port of the 7.15 unit (audit/715-dice-only @ bedb6ee) to the bitcoin-only
product, which the owner named as the primary target for this feature.

dice_input.c/.h, dice.cpp, the capture script, the verifier and both docs are
byte-identical to the 7.15 unit. reset.c is identical except where the two
products already differed: 7.15 shares the mnemonic pager's scratch with its
BIP-85 flow through file-scope buffers, and this product has no BIP-85, so
show_mnemonic_pages() owns its scratch as function-local statics here, as the
backup pager did before it was factored. Nothing else diverges.

What the unit does, on both products:
  - ResetDevice.dice_entropy alone is MIXED: the device shows its 32-byte draw
    as 24 BIP-39 words BEFORE the rolls are entered, then
    seed = SHA256d("KK\x01SM" || draw || SHA256("KK\x01D" || rolls)).
  - dice_entropy + dice_only is ONLY: seed = SHA256(rolls). Coldcard's
    Dice-Rolls-Only byte for byte. dice_only without dice_entropy is refused.
  - The device shows a consent screen naming the mode the host asked for;
    holding proceeds, the only "no" is cancelling the reset.
  - The host's EntropyAck is consumed and its bytes dropped in both modes, so
    the wire flow is unchanged. The default no-dice reset is untouched.
  - Rolls with any face over 30% are refused before a digest is drawn; the
    digest is shown in full; tools/verify_dice_seed.py recomputes the wallet
    offline from the roll string (plus the 24 words for MIXED).

Pins, as on the 7.15 unit:
  deps/device-protocol -> 5517c8de3 (feat/dice-only-field-7.14.3): the
    dice_only field cherry-picked onto this product's current pin 8545cd5b6.
    Not canonical up/release-protocol, because the twelve commits between
    carry EIP-712 streaming fields with no .options here and the
    pb_callback_t gate would fail. Retires when the product advances its pin.
  deps/python-keepkey -> c0e9bd8 (feat/dice-modes): host tests for both modes.
Findings from an adversarial review of the unit (86 agents, 3 refuters per
finding) and from the first CI runs, all confirmed against the code:

- The full 7.15 image failed to link: "Insufficient runtime SRAM: require 16
  KiB stack/heap reserve". Its base reserve was 16,420 B against a 16,384 B
  floor, and the unit had added a 128-byte static for the digest page. That
  page is now formatted into current_words -- 265 bytes, already
  CONFIDENTIAL, idle between roll entry and the backup pager -- so the unit
  adds no static RAM beyond the 4-byte mode.
- dice with no_backup is refused with SyntaxError before any screen, as
  display_random with no_backup was: the modes exist to be checked against
  the backup words, and a reset that never shows them would put seed material
  on the OLED under a WARNING that recovery is impossible.
- The MIXED consent screen now says the 24 entropy words are NOT a backup,
  since the pages that follow are otherwise indistinguishable from the backup
  pages. Both consent bodies are shortened so neither can ever paginate.
- Features.supports_dice_modes (28) is reported. Older firmware skips the
  unknown dice_only field and derives a different wallet without complaint,
  so hosts and the test suite gate on this bit rather than on a version.
- dice_mix() was dead once the modes replaced it; removed with its tests. The
  non-collision test now compares against the old formula's Python-computed
  vector instead. A unit test that placed its invalid byte past `count` is
  corrected.
- tools/verify_dice_seed.py looked for a wordlist file the repo does not
  ship; it now reads the firmware's own bip39_english.c (slicing "abandon"
  to "zoo", since the file quotes other strings too) and was checked to yield
  exactly the reference English list.
- Stale comments describing an on-device mode selector are corrected;
  docs/DiceEntropy.md "What happens" now describes the shipped ceremony.

Pins: deps/device-protocol -> 0848887cf04a67342c468a40b36fa67d15cdca9b (feat/dice-only-field-7.14.3, adds
Features.supports_dice_modes = 28 on the products' dp base);
deps/python-keepkey -> 51d7cd112aee21daea44c124310c7b08e921ee07 (feat/dice-modes: capability-gated tests, the
digest-subpage and roll-injection driver fixes, no_backup and consent-cancel
tests, atlas entries).
feat/dice-modes @ 03c2fe11f704720c7a0230908ae9971717765aa5: the report atlas names the unit's eight Dice gtests
in place of the four removed with dice_mix(), which the 7.15 bitcoin-only
catalog validation had flagged as missing.
`git submodule update --remote` moves a submodule to the tip of the branch
named in .gitmodules. This unit pins device-protocol and python-keepkey to the
dice branches, while the keys still named `master` and `reconcile/upstream-sync`
-- neither of which contains those commits, so the documented command would
silently drop the dice proto field and the dice host tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V1RPHNqU3JmYFwBYvEKxKn
Two defects in the offline verification story, which is the entire reason
these modes exist.

The tool inferred the ceremony from whether --device-words was passed. A user
who ran MIXED and forgot the flag got a DICE ONLY derivation -- a perfectly
valid, completely different mnemonic -- followed by "the device did not derive
the wallet from your rolls. Do not fund it." That is a fraud accusation caused
by an omitted argument, and a warning that fires on user error is a warning
people learn to ignore. --mode now says it outright, the two impossible
combinations are refused with the reason, and the closing text tells the
reader to rule out the mode before concluding anything about the device.

DiceEntropy.md published the domain tags as `"KK\x01D"` and `"KK\x01SM"`.
Written as a C string literal that is wrong: `\x` is greedy, so `\x01D` is one
character, 0x1D, and a reader who reimplements the derivation from the
published spec gets three bytes where the firmware uses four and can never
reproduce an honest device's wallet. The tags are now published as bytes, with
the trap named so nobody re-derives it.

Checked against the firmware arrays in dice_input.c and the tool constants:
4B 4B 01 44 and 4B 4B 01 53 4D, byte for byte.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V1RPHNqU3JmYFwBYvEKxKn
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants